package cn.alex.sso.framework.config.satoken;

import cn.alex.sso.framework.enums.StatusEnum;
import cn.alex.sso.framework.global.SaSsoExceptionMsg;
import cn.alex.sso.framework.util.StreamUtils;
import cn.alex.sso.project.entity.SysClient;
import cn.alex.sso.project.entity.SysClientAllow;
import cn.alex.sso.project.service.SysClientAllowService;
import cn.alex.sso.project.service.SysClientService;
import cn.dev33.satoken.config.SaSignConfig;
import cn.dev33.satoken.sign.SaSignTemplate;
import cn.dev33.satoken.sso.error.SaSsoErrorCode;
import cn.dev33.satoken.sso.template.SaSsoServerTemplate;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

/**
 * 自定义 SaSsoServerTemplate 子类
 *
 * @author Alex
 * @date 2024/10/16 23:12
 */
@Component
@RequiredArgsConstructor
public class CustomSaSsoServerTemplate extends SaSsoServerTemplate {
    private final SysClientService clientService;
    private final SysClientAllowService clientAllowService;

    /**
     * 获取：所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
     *
     * @return see note
     */
    @Override
    public String getAllowUrl() {
        String allowUrl = "";
        List<SysClientAllow> clientList = clientAllowService.getAllowListByUserId(StpUtil.getLoginIdAsLong());
        if (CollUtil.isNotEmpty(clientList)) {
            allowUrl = StreamUtils.join(clientList, SysClientAllow::getUrl, ",");
        }
        return allowUrl;
    }

    /**
     * 校验重定向url合法性
     *
     * @param url 下放ticket的url地址
     */
    @Override
    public void checkRedirectUrl(String url) {
        // 1、是否是一个有效的url
        if (!SaFoxUtil.isUrl(url)) {
            throw new SaSsoExceptionMsg("无效回调地址：" + url).setCode(SaSsoErrorCode.CODE_30001);
        }

        // 2、截取掉?后面的部分
        int qIndex = url.indexOf("?");
        if (qIndex != -1) {
            url = url.substring(0, qIndex);
        }

        // 3、不允许出现@字符
        if (url.contains("@")) {
            //  为什么不允许出现 @ 字符呢，因为这有可能导致 redirect 参数绕过 AllowUrl 列表的校验
            //
            //  举个例子 配置文件：
            //       sa-token.sso-server.allow-url=http://sa-sso-client1.com*
            //
            //  开发者原意是为了允许 sa-sso-client1.com 下的所有地址都可以下放ticket
            //
            //  但是如果攻击者精心构建一个url：
            //       http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com@sa-token.cc
            //
            //  那么这个url就会绕过 allow-url 的校验，ticket 被下发到了第三方服务器地址：
            //       http://sa-token.cc/?ticket=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul
            //
            //  造成了ticket 参数劫持
            //  所以此处需要禁止在 url 中出现 @ 字符
            //
            //  这么一刀切的做法，可能会导致一些特殊的正常url也无法通过校验，例如：
            //       http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com:9003/@getInfo
            //
            //  但是为了安全起见，这么做还是有必要的
            throw new SaSsoExceptionMsg("无效回调地址（不允许出现@字符）：" + url).setCode(SaSsoErrorCode.CODE_30001);
        }

        // 4、判断是否在 [ 允许的地址列表 ] 之中
        List<String> allowUrlList = Arrays.asList(getAllowUrl().replaceAll(" ", "").split(","));
        checkAllowUrlList(allowUrlList);
        if (!SaStrategy.instance.hasElement.apply(allowUrlList, url)) {
            throw new SaSsoExceptionMsg("您无权限访问当前地址，请联系管理员。访问地址：" + url).setCode(SaSsoErrorCode.CODE_30002);
        }

        // 校验通过 √
    }

    /**
     * 获取底层使用的 API 签名对象
     *
     * @param client 指定客户端标识，填 null 代表获取默认的
     * @return /
     */
    @Override
    public SaSignTemplate getSignTemplate(String client) {
        long clientId;
        try {
            clientId = Long.parseLong(client);
        } catch (NumberFormatException e) {
            throw new SaSsoExceptionMsg("APPID无效：" + client).setCode(SaSsoErrorCode.CODE_30013);
        }

        SysClient c = clientService.getById(clientId);
        if (ObjUtil.isEmpty(c)) {
            throw new SaSsoExceptionMsg("APPID无效：" + client).setCode(SaSsoErrorCode.CODE_30013);
        }

        if (c.getStatus().equals(StatusEnum.NO.getStatus())) {
            throw new SaSsoExceptionMsg("应用已被禁用：" + c.getName()).setCode(30016);
        }

        return new SaSignTemplate(new SaSignConfig(c.getSecretKey()));
    }
}
